🌳 Indexação e Árvore B+

Objetivo: classificar índices, estimar quantos blocos uma busca lê e, principalmente, montar à mão uma árvore B+: calcular quantas entradas cabem no nó, inserir com split, remover com empréstimo/unsplit e desenhar o arquivo do índice.


🧭 Comece aqui

A ideia em uma frase. Pense no índice remissivo no fim de um livro. Em vez de ler o livro inteiro para achar um assunto, você procura o assunto numa lista em ordem alfabética, e ela diz a página. O índice do banco é igual: uma lista ordenada de <chave, rid>, em que o rid diz em que bloco do arquivo de dados está a linha. A árvore B+ é essa lista organizada em níveis: a raiz diz em que parte procurar, e as folhas guardam as chaves com os rids. Cada nó é um bloco, então descer um nível é ler 1 bloco. Com 2 ou 3 níveis, o banco acha qualquer chave lendo 2 ou 3 blocos, em vez do arquivo inteiro.

O que a questão pede. O desenho da árvore B+ depois de uma sequência de INSERT e DELETE, com os nós numerados, as entradas e as setas, e o desenho do arquivo do índice (que bloco é cada nó).

Palavras que vão aparecer:

Onde este arquivo se encaixa. É o 2º de 3. Os rids das folhas vêm do arquivo de dados ("Banco de Dados: Organização de Arquivos"). O número de níveis (xx) e de folhas (bleafb_{leaf}) do índice entram nas contas de "Banco de Dados: Custo de Consultas".

Se você está perdido, leia nesta ordem:

  1. Este "Comece aqui".
  2. A seção 4 (estrutura e como uma busca anda na árvore).
  3. O exemplo clássico (seção 11), que é pequeno e mostra todos os tipos de split.
  4. O exemplo resolvido completo (seção 12), acompanhando a Receita (seção 10).

As seções 2 e 3 são teoria de consulta; dá para deixá-las por último.


🗂️ 1. O que é um índice


🏷️ 2. Classificação

Índice Arquivo ordenado pelo atributo? Atributo único? Denso ou esparso Como busca
Primário Sim Sim Esparso: 1 entrada por bloco (o 1º registro do bloco é a âncora) Acha o bloco (maior chave ≤ K) e procura dentro dele.
Clustering Sim Não Esparso: 1 entrada por valor distinto (ou âncora por bloco) Acha o 1º bloco do valor e lê até o valor acabar.
Secundário único Não Sim Denso: 1 entrada por registro Chave → rowId → registro.
Secundário não único Não Não Denso (chave repetida, um rowId cada) ou chave + lista de rowIds (um nível a mais de indireção) Igual, com 1 acesso por registro encontrado.

⏱️ 3. Quanto custa buscar (exemplo resolvido)

Se está começando, pule para a seção 4 e volte aqui depois. Esta seção compara números de acessos e usa os termos da seção 2.

Exemplo clássico do livro de Elmasri & Navathe. O arquivo tem r=30 000r = 30\,000 registros de R=100R = 100 B, em blocos de B=1024B = 1024 B (unspanned). Uma entrada de índice tem chave de 9 B + ponteiro de 6 B = 15 B.

Bfr=⌊1024100⌋=10b=30 00010=3 000Bfri=⌊102415⌋=68Bfr = \left\lfloor \frac{1024}{100} \right\rfloor = 10 \qquad b = \frac{30\,000}{10} = 3\,000 \qquad Bfr_i = \left\lfloor \frac{1024}{15} \right\rfloor = 68

Bfri=68Bfr_i = 68 é o fan-out: quantas entradas de índice cabem num bloco.

Cenário Conta Acessos
Arquivo ordenado pelo atributo, busca binária no arquivo ⌈log⁡23000⌉\lceil \log_2 3000 \rceil 12
Índice primário mononível (esparso: 3 000 entradas, uma por bloco) b′=⌈3000/68⌉=45b' = \lceil 3000/68 \rceil = 45, então ⌈log⁡245⌉+1\lceil \log_2 45 \rceil + 1 7
Arquivo não ordenado, atributo com d=5d = 5 repetidos em média: busca sequencial bb 3 000
Índice secundário mononível denso (30 000 entradas) b′=⌈30000/68⌉=442b' = \lceil 30000/68 \rceil = 442, então ⌈log⁡2442⌉+5\lceil \log_2 442 \rceil + 5 14
Índice secundário multinível 442 → ⌈442/68⌉=7\lceil 442/68 \rceil = 7 → ⌈7/68⌉=1\lceil 7/68 \rceil = 1: x=3x = 3 níveis, então 3+53 + 5 8

🌲 4. Árvore B+: estrutura

🔎 Como uma busca anda na árvore

Esta é a árvore final do exemplo resolvido (seção 12). Cada folha guarda chave rid, e a última linha é a lista encadeada das folhas:

                n3 ( CR3 | GN2 | NB2 | W10 )
      ┌───────────┬───────────┼───────────┬───────────┐
      ▼           ▼           ▼           ▼           ▼
     n1          n7          n5          n6          n4
  AMP 0202    GE2 0302    MSP 0105    RSR 0204    WFL 0203
  AZA 0102    GN2 0205    NB2 0103    W10 0101    WRF 0104
  CR3 0303                                        WRP 0201
     n1 ───────► n7 ───────► n5 ───────► n6 ───────► n4 ──► NULL

Buscar RSR (busca pontual). Na raiz, compare RSR com os separadores da esquerda para a direita e desça pelo primeiro cuja chave é ≥ RSR:

  1. RSR > CR3, RSR > GN2, RSR > NB2, e RSR ≤ W10. Desça pela seta à esquerda do W10 até o n6.
  2. Na folha n6 está RSR 0204: a linha está no bloco 02, slot 04 do arquivo de dados.
  3. Total: 2 blocos de índice (n3, n6) + 1 bloco de dados = 3 acessos, em vez de ler o arquivo inteiro.

Por que "≤ vai para a esquerda". O separador é a maior chave da subárvore da esquerda: NB2 é a última chave de n5. Para buscar o próprio NB2, compare NB2 ≤ NB2: verdadeiro, então desça à esquerda e chegue no n5. Por isso, no split, é a maior chave da metade esquerda que sobe.

Buscar de GE2 até NB2 (busca por intervalo). Desça até a folha do GE2 (n7) como na busca pontual. Depois siga a lista encadeada: n7 (GE2, GN2) → n5 (MSP, NB2), e pare quando passar de NB2. Não precisa voltar à raiz, e é para isso que as folhas são encadeadas.

E quando insere ou remove? A árvore precisa continuar assim: chaves em ordem, todas as folhas no mesmo nível e cada nó com pelo menos metade da capacidade. O split (nó cheio demais) e o unsplit (nó vazio demais) são os consertos que mantêm isso. As seções 7 e 8 mostram como fazer cada um.


📐 5. Capacidade do nó

Cada nó ocupa um bloco de BB bytes. A chave tem KK bytes, o rowId tem ridrid bytes e o ponteiro de árvore tem PP bytes. Sobra sempre um ponteiro: na folha é o da próxima folha, e no nó interno é o ponteiro mais à direita.

entradas na folha=⌊B−PK+rid⌋chaves no noˊ interno=⌊B−PK+P⌋  (ponteiros=chaves+1)\text{entradas na folha} = \left\lfloor \frac{B - P}{K + rid} \right\rfloor \qquad \text{chaves no nó interno} = \left\lfloor \frac{B - P}{K + P} \right\rfloor \;(\text{ponteiros} = \text{chaves} + 1)

Chave (B=256B = 256, rid=64rid = 64, P=32P = 32) KK Folha Nó interno
CHAR(3), 2 B por caractere 6 B ⌊224/70⌋=3\lfloor 224/70 \rfloor = 3 ⌊224/38⌋=5\lfloor 224/38 \rfloor = 5 chaves, 6 ponteiros
INT de 8 B 8 B ⌊224/72⌋=3\lfloor 224/72 \rfloor = 3 ⌊224/40⌋=5\lfloor 224/40 \rfloor = 5 chaves, 6 ponteiros
CHAR(10), 2 B por caractere 20 B ⌊224/84⌋=2\lfloor 224/84 \rfloor = 2 ⌊224/52⌋=4\lfloor 224/52 \rfloor = 4 chaves, 5 ponteiros

🔤 6. Ordem das chaves

AMP < AZA < CR3 < GE2 < GN2 < MSP < NB2
    < RSR < RV6 < RV7 < W10 < WFL < WRF < WRP

➕ 7. Inserção

  1. Desça da raiz: em cada nó interno, siga o primeiro ponteiro cuja chave é ≥ X. Se X é maior que todas, siga o último.
  2. Insira na folha, em ordem.
  3. Folha estourou (capacidade + 1 entradas): faça o split. O nó original (que mantém o número) fica com a metade esquerda, ⌈n/2⌉\lceil n/2 \rceil entradas, e a direita vai para um nó novo, com o próximo número. Encadeie: esquerda → nova → antiga próxima.
  4. Sobe uma cópia da maior chave da esquerda, que continua na folha, como separador no pai.
  5. Nó interno estourou: faça o split. A chave do meio sobe e sai do nó, e a esquerda fica com pelo menos tantas chaves quanto a direita. Exemplo: 6 chaves num nó de 5 → esquerda com 3, sobe a 4ª, direita com 2.
  6. Raiz estourou: nasce uma raiz nova (próximo número) com 1 chave e 2 ponteiros, e a altura cresce. O header passa a apontar para ela.

🔑 Na folha a chave é copiada e fica nos dois lugares. No nó interno a chave se move.


➖ 8. Remoção

  1. Ache a folha e remova a entrada. A chave continua nos nós internos como separador, porque ela ainda separa certo.
  2. Sem underflow: fim.
  3. Underflow → tente emprestar de um irmão com o mesmo pai, primeiro o esquerdo. O irmão só empresta se continuar com o mínimo. Do esquerdo vem a maior entrada; do direito, a menor. O separador entre os dois passa a ser a maior chave do nó da esquerda.
  4. Ninguém pode emprestar → unsplit (junta com um irmão). Escolha o irmão pela premissa do enunciado. Exemplo: "o nó da esquerda nunca fica com menos entradas que o da direita". O pai perde o separador entre os dois e o ponteiro do nó liberado. O encadeamento das folhas pula o nó liberado.
  5. O bloco liberado continua no arquivo, marcado como livre.
  6. Pai em underflow: repita um nível acima. No empréstimo entre internos, a chave passa pelo pai; na junção de internos, o separador do pai desce para o nó juntado. Se a raiz ficar sem chave, o filho único vira a raiz e a altura diminui.

💽 9. O arquivo do índice


✅ 10. Receita na prova

  1. Capacidade da folha e do nó interno (seção 5) e o mínimo antes do underflow.
  2. Lista ordenada de todas as chaves, pela regra do enunciado.
  3. Insira uma por uma numa tabela com as colunas # | chave e rid | folha | o que acontece | raiz depois. O passo a passo vale nota parcial.
  4. Desenhe a árvore antes das remoções.
  5. Remoções: underflow? Empreste (esquerdo primeiro); senão, unsplit pela premissa. Os separadores ficam.
  6. Árvore final: nós numerados, rid em cada entrada de folha e setas da lista de folhas até NULL.
  7. Arquivo do índice: blocos na ordem de criação, header e blocos LIVRE.
  8. Escreva as regras que aplicou (desempate de split, irmão do unsplit).

📝 11. Exemplo clássico: ordem 3

Livro de Elmasri & Navathe: árvore B+ de ordem 3 (p=3p = 3: nó interno com até 3 ponteiros e 2 chaves; pfolha=2p_{\text{folha}} = 2 entradas por folha). Inserções: 8, 5, 1, 7, 3, 12, 9, 6. Os nós são numerados na ordem de criação (n1,n2,…n_1, n_2, \dots). É o exemplo que mostra o split de nó interno e o da raiz.

INSERT 8   n1 [8]
INSERT 5   n1 [5 8]                                       ← folha cheia
INSERT 1   [1 5 8] estoura → n1 [1 5] | n2 [8], sobe 5 → nova raiz n3

                  n3 ( 5 )
                 /        \
           n1 [1 5]  →  n2 [8]

INSERT 7   n2 [7 8]
INSERT 3   3 ≤ 5 → [1 3 5] estoura → n1 [1 3] | n4 [5], sobe 3

                  n3 ( 3 | 5 )
                /      |      \
          n1 [1 3] → n4 [5] → n2 [7 8]

INSERT 12  [7 8 12] estoura → n2 [7 8] | n5 [12], sobe 8
           n3 ( 3 | 5 | 8 ) estoura (máx. 2 chaves): 5 SOBE e sai,
           n3 ( 3 ) | n6 ( 8 ), nova raiz n7

                         n7 ( 5 )
                    /               \
              n3 ( 3 )             n6 ( 8 )
             /        \           /        \
       n1 [1 3] → n4 [5] → n2 [7 8] → n5 [12]

INSERT 9   n5 [9 12]
INSERT 6   6 > 5, 6 ≤ 8 → [6 7 8] estoura → n2 [6 7] | n8 [8], sobe 7
           n6 ( 7 | 8 ): cabe

Árvore final:

flowchart TB R["n7 (raiz)<br/>5"] R --- I3["n3<br/>3"] R --- I6["n6<br/>7 | 8"] I3 --- L1["n1<br/>1 · 3"] I3 --- L4["n4<br/>5"] I6 --- L2["n2<br/>6 · 7"] I6 --- L8["n8<br/>8"] I6 --- L5["n5<br/>9 · 12"]
flowchart LR n1["n1"] --> n4["n4"] --> n2["n2"] --> n8["n8"] --> n5["n5"] --> X[NULL]

📝 12. Exemplo resolvido completo

Premissas: blocos de 256 B; chave id CHAR(3) com 2 B por caractere (6 B); rid de 64 B; ponteiro de árvore de 32 B; valores ≤ a chave ficam à esquerda; em split/unsplit, o nó esquerdo nunca fica com menos entradas que o direito; dígitos < letras; folhas em lista simplesmente encadeada; o header guarda o nº de blocos, a raiz e a folha mais à esquerda. O índice é o da chave primária de product, e os rids vêm do arquivo de dados (ver o cheat sheet "Banco de Dados: Organização de Arquivos").

Instruções: 13 INSERT (W10, AZA, NB2, WRF, MSP, WRP, RV6, WFL, RSR, GN2, RV7, GE2, CR3), depois DELETE de RV6 e RV7, depois INSERT de AMP (rid 0202).

Passo 1. Capacidade. Folha: ⌊(256−32)/(6+64)⌋=⌊3,2⌋=3\lfloor (256 - 32)/(6 + 64) \rfloor = \lfloor 3{,}2 \rfloor = 3 entradas. Interno: ⌊(256−32)/(6+32)⌋=⌊5,9⌋=5\lfloor (256 - 32)/(6 + 32) \rfloor = \lfloor 5{,}9 \rfloor = 5 chaves e 6 ponteiros. Uma folha entra em underflow com 1 entrada.

Passo 2. Ordem das chaves. AMP < AZA < CR3 < GE2 < GN2 < MSP < NB2 < RSR < RV6 < RV7 < W10 < WFL < WRF < WRP (a pegadinha é W10 < WFL).

Passo 3. As 13 inserções. 4 entradas numa folha de 3 dividem 2 e 2.

# INSERT Folha O que acontece Raiz (n3)
1 W10 0101 1 n1 nasce: folha e raiz ao mesmo tempo —
2 AZA 0102 1 [AZA W10] —
3 NB2 0103 1 [AZA NB2 W10], cheia —
4 WRF 0104 1 SPLIT: n1 [AZA NB2], n2 [W10 WRF]; sobe NB2; nasce a raiz n3 NB2
5 MSP 0105 1 [AZA MSP NB2] NB2
6 WRP 0201 2 [W10 WRF WRP] NB2
7 RV6 0202 2 SPLIT: n2 [RV6 W10], n4 [WRF WRP]; sobe W10 NB2 W10
8 WFL 0203 4 [WFL WRF WRP] NB2 W10
9 RSR 0204 2 [RSR RV6 W10] NB2 W10
10 GN2 0205 1 SPLIT: n1 [AZA GN2], n5 [MSP NB2]; sobe GN2 GN2 NB2 W10
11 RV7 0301 2 SPLIT: n2 [RSR RV6], n6 [RV7 W10]; sobe RV6 GN2 NB2 RV6 W10
12 GE2 0302 1 [AZA GE2 GN2] GN2 NB2 RV6 W10
13 CR3 0303 1 SPLIT: n1 [AZA CR3], n7 [GE2 GN2]; sobe CR3 CR3 GN2 NB2 RV6 W10

Passo 4. Antes das remoções (raiz com 5 chaves, cheia; folhas encadeadas n1 → n7 → n5 → n2 → n6 → n4):

               n3 ( CR3 | GN2 | NB2 | RV6 | W10 )
     ┌──────────┬──────────┬────┴─────┬──────────┬──────────┐
     ▼          ▼          ▼          ▼          ▼          ▼
    n1         n7         n5         n2         n6         n4
 AZA|0102   GE2|0302   MSP|0105   RSR|0204   RV7|0301   WFL|0203
 CR3|0303   GN2|0205   NB2|0103   RV6|0202   W10|0101   WRF|0104
                                                        WRP|0201

Passo 5. Remoções.

juntar com o ESQUERDO:  n5 [MSP NB2 RSR] | n6 [RV7 W10]       3 ≥ 2  respeita
juntar com o DIREITO:   n5 [MSP NB2]     | n2 [RSR RV7 W10]   2 < 3  VIOLA

O n5 absorve o n2, que é liberado. A raiz perde o separador entre eles (NB2) e o ponteiro para o n2. RV6 continua na raiz, porque separador não se apaga, e ela ainda separa certo (RSR ≤ RV6 < RV7). Raiz: ( CR3 | GN2 | RV6 | W10 ).

Passo 6. INSERT AMP (0202). AMP ≤ CR3 → n1 [AMP AZA CR3]: fica cheia, sem split.

Resposta: árvore final.

flowchart TB R["n3 (raiz)<br/>CR3 | GN2 | NB2 | W10"] R --- L1["n1<br/>AMP · 0202<br/>AZA · 0102<br/>CR3 · 0303"] R --- L7["n7<br/>GE2 · 0302<br/>GN2 · 0205"] R --- L5["n5<br/>MSP · 0105<br/>NB2 · 0103"] R --- L6["n6<br/>RSR · 0204<br/>W10 · 0101"] R --- L4["n4<br/>WFL · 0203<br/>WRF · 0104<br/>WRP · 0201"]
flowchart LR n1["n1"] --> n7["n7"] --> n5["n5"] --> n6["n6"] --> n4["n4"] --> X[NULL]
Ponteiro da raiz Faixa Folha Chaves
1º k ≤ CR3 n1 AMP, AZA, CR3
2º CR3 < k ≤ GN2 n7 GE2, GN2
3º GN2 < k ≤ NB2 n5 MSP, NB2
4º NB2 < k ≤ W10 n6 RSR, W10
5º k > W10 n4 WFL, WRF, WRP

São 12 chaves, as mesmas 12 tuplas do arquivo de dados. Foram criados 7 nós, e um foi liberado (n2).

Resposta: arquivo do índice.

header: nº de blocos = 7 | raiz = bloco 3 | folha mais à esquerda = bloco 1

 bloco   1       2       3       4       5       6       7
     ┌───────┬───────┬───────┬───────┬───────┬───────┬───────┐
     │ folha │ LIVRE │ RAIZ  │ folha │ folha │ folha │ folha │
     └───────┴───────┴───────┴───────┴───────┴───────┴───────┘
                 ▲ liberado pelo unsplit do DELETE RV6

 ordem lógica das folhas: 1 → 7 → 5 → 6 → 4 → NULL

⚠️ 13. Pegadinhas


🔁 14. Variações que podem cair

Se o enunciado pedir… Faça
Índice de outro atributo (ex.: UNIQUE (name) em CHAR(10)) Recalcule a capacidade com o novo KK: 2 por folha e 4 chaves por nó interno no exemplo. Ordene pelas novas chaves e use os mesmos rids do arquivo de dados.
Índice de chave INT K=8K = 8 B; compare como número.
Muitas inserções seguidas Espere o split do nó interno e o split da raiz (exemplo clássico, seção 11).
Remoção que esvazia o pai Propague: empréstimo/junção entre nós internos (o separador do pai desce) e, no limite, a raiz encolhe.
"≥ vai para a esquerda" ou "esquerda ≤ direita" Inverta a regra de desempate e diga isso na resposta.
Árvore B (não B+) As chaves dos nós internos não se repetem nas folhas, e cada chave aparece uma vez com o seu rowId.
Índice secundário não único A chave se repete, uma entrada por rowId (ou chave + lista de rowIds).